<?php
/*
 **********************************************
 JCal Pro
 Copyright (c) 2006-2009 Anything-Digital.com
 **********************************************
 JCal Pro is a fork of the existing Extcalendar component for Joomla!
 (com_extcal_0_9_2_RC4.zip from mamboguru.com).
 Extcal (http://sourceforge.net/projects/extcal) was renamed
 and adapted to become a Mambo/Joomla! component by
 Matthew Friedman, and further modified by David McKinnis
 (mamboguru.com) to repair some security holes.

 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or
 (at your option) any later version.

 This header must not be removed. Additional contributions/changes
 may be added to this header as long as no information is deleted.
 **********************************************

 $Id: events.php 352 2009-06-17 13:34:13Z shumisha $

 **********************************************
 Get the latest version of JCal Pro at:
 http://dev.anything-digital.com//
 **********************************************
 */

// Check to ensure this file is included in Joomla!
defined('_JEXEC') or die( 'Restricted access' );

jimport('joomla.application.component.model');

/**
 * iCal import/export model
 *
 */
class JcalproModelIcal extends JModel {

// the various parameters needed to fetch events from DB
  private $_params = null;

  // the vcalendar object being worked upon
  private $_cal = null;

  // store timezones definition found in the ics file
  private $_timezones = null;

  /**
   * Constructor set default values for some parameters
   * @return none
   */
  public function __construct() {

  // as we actually have some events to return, require ical library
    require_once(JPATH_ROOT.DS. 'components' . DS . 'com_jcalpro' . DS .'lib'.DS.'iCalcreator.class.php');

  }

  /**
   * Inject private properties into the object
   *
   * @param properties an array holding the names/values pairs
   * @return none
   */
  public function injectProperties( $properties) {

    if (!empty( $properties)) {
      foreach( $properties as $name => $value) {
        $this->_params->$name = $value;
      }
    }
  }

  /**
   * Read iCal events from a local file or remote url
   * parses them, and store them into DB
   *
   * @param $storeIntoDb boolean true if need to store parsed event into db as JCal events
   * @return mixed : boolean false if error occured | true if parsing events only
   *                 integer the number of events imported into db if importing to db
   */
  public function importFromLocation( $storeIntoDb = true) {

    global $mainframe;

    // create a calendar object, and ask iCalCreator to parse the file
    $this->_cal = new vcalendar();

    // store target file/url in iCalCreator object
    if(isset($this->_params->directory)) {
      $this->_cal->setConfig( 'directory', $this->_params->directory);
    }
    if (isset($this->_params->filename)) {
      $this->_cal->setConfig( 'filename', $this->_params->filename);
    }
    if (isset($this->_params->url)) {
      $this->_cal->setConfig( 'url', $this->_params->url);
    }

    // need to set unique id. In case a UID does not exist in the
    // file, it will be created using this unique_id
    $this->_cal->setConfig( 'unique_id', $this->_params->unique_id);

    // let iCalCreator parse the file into the calendar object
    $parseResult = $this->_cal->parse();

    // if error, return
    if (!$parseResult) {
      $mainframe->enqueueMessage( 'Error while parsing ics file content.', 'error');
    }

    // import the iCal calendar into JCalPro database
    if ($parseResult && $storeIntoDb) {
      $parseResult = $this->_iCalImportToDB();
    }

    // send feedback
    return $parseResult;
  }

  /**
   * Turn a iCalCreator calendar object
   * into records in jCalPro database
   *
   * @return integer, the number of events imported, or false if error
   */
  private function _iCalImportToDB() {

    global $mainframe;

    // a counter
    $eventsInFile = 0;
    $eventsInDB = 0;

    // iterate through parsed events from ical file
    do {
      $c = $this->_cal->getComponent();
      $status = true;
      if (!empty( $c)) {
        if ($c instanceof vevent) {
        // we have one more event from ics file, increase counter
          $eventsInFile++;
          // try storing it in database
          $status = $this->_storeiCalEventInDB( $c);
          // if success, increase imported events counter
          if ($status) {
            $eventsInDB++;
          }
        }
        if ($c instanceof vtimezone) {
          $tzid = stripslashes($this->_getICalProperty( $c, 'tzid'));
          $zones = array();
          do {
            $zone = $c->getComponent();
            if (!empty( $zone)) {
              $zones[] = $zone;
            }
          } while (!empty($zone));
          if (!empty( $zones)) {
            $this->_timezones[$tzid] = $zones;
          }
        }
      }
    // repeat until no more events in ics file, or error occured while storing
    } while (!empty($c));

    // display feedback
    $msg = (empty( $eventsInFile) ? 'No' : $eventsInFile) .  ' events found in file/URL.';
    $mainframe->enqueueMessage( $msg);
    $msg = (empty( $eventsInDB) ? 'No' : $eventsInDB) .  ' events stored in database.';
    $mainframe->enqueueMessage( $msg);

    // send feedback
    return $status  ? $eventsInDB : false;
  }

  /**
   * Transform a supplied ical calendar component object into
   * its jcal equivalent and store it into the database
   *
   * @param $c a calendarComponent object from iCalCreator.class.php
   * @return boolean true upon success
   */
  private function _storeiCalEventInDB( calendarComponent $c) {

    static $categoryNames = null;

    global $mainframe;

    // retrieve and store Jcal pro categories names list
    // so as to be able to match jcal categories to those
    // found in the imported ical file
    if (is_null( $categoryNames)) {
    // not yet fetched the categories
      $categories = jclGetCategories();
      $categories = explode( ',', $categories);
      // store category names in an array indexed by category id
      foreach( $categories as $category) {
      // only add a category to the list if user has access to it
        if ( has_priv ( 'category' . $category)) {
          $categoryNames[$category] = JString::strtolower( jclGetCategoryName( $category));
        }
      }
    }

    // try creating event

    // find about the category in which to put the event
    // for now, events can be stored in only one category, so if the event
    // has several CATEGORIES properties, we'll only use the first
    if ($this->_params->guessCat) {
    // admin asked to use the CATEGORIES property to try find the category for the event
      $eventCat =  $this->_getICalProperty( $c, 'CATEGORIES');
      if (!is_null($eventCat)) {
        if (is_array($eventCat)) {
          $eventCat = JString::strtolower(JString::trim($eventCat[0]));
        }
        // event record has at least one CATEGORIES property. Does it match one of ours ?
        foreach( $categoryNames as $catId => $catName) {
          if ($catName == $eventCat) {
          // we have a match, adjust category id target
            $this->_params->targetCat = $catId;
            break;
          }
        }
      }
    }

    // set the static parts of the event
    $this->_setEventStaticElements( $event, $c);

    // set everything dealing with recurrency
    $this->_setEventRecurrencyElements( $event, $c);

    // common id : we use that of the original event. If there is none, it will be created
    // automatically when the event is stored to DB anyway
    // trim it, as there may be some trailing \r for instance
    $event->common_event_id = $this->_cleanUID($this->_getICalProperty( $c, 'uid'));

    // we have our object, find if this event already exists in the db, in order to update it
    // if so. We use the common_event_id to do that, as it is unique across the board
    $previousEvent = jclFindEventByCommonId($event->common_event_id, $noChild = true);
    $event->extid = empty($previousEvent) ? null : $previousEvent->extid;

    if (!empty($event->extid)) {
    // this event already exists, update it. There are several cases to consider
      if ($previousEvent->rec_type_select == JCL_REC_TYPE_NONE && $event->rec_type_select == JCL_REC_TYPE_NONE ) {
      // was non-recurrent and stays non-recurrent, only update the event
        $status = updateExistingEvent( $event);
      } else if ($previousEvent->rec_type_select != JCL_REC_TYPE_NONE) {
        // was a parent recurring event, and is now either recurrent or static
          $status = updateExistingRecurringEvent( $event);
        } else {
        // last case : was a static, and now becomes recurrent
          $status = updateExistingStaticEventToRecurring( $event);
        }
    }

    if (empty($event->extid)) {
    // this event does not already exist, create it
    // this is simply storing, as we do not handle recurring events importing
    // at this stage
    //$eventId = storeNewEvent( $event);

      $rawData = get_object_vars( $event);
      $successful = createEvent( $event->cal_id, $event->owner_id, $event->title, $event->description, $event->contact, $event->url,
          $event->registration_url, $event->email, $event->picture,
          $event->cat, $event->day, $event->month, $event->year, $event->approved, $event->private, $event->start_date, $event->end_date,
          $event->published, $event->recur_end_type,
          $event->recur_count, $event->recur_until, $rawData, $event->rec_id = 0,
          $event->detached_from_rec = 0, $checkOnly = true);

      if ($successful) {
        $successful = createEvent( $event->cal_id, $event->owner_id, $event->title, $event->description, $event->vontact, $event->url,
            $event->registration_url, $event->email, $event->picture,
            $event->cat, $event->day, $event->month, $event->year, $event->approved, $event->private, $event->start_date, $event->end_date,
            $event->published, $event->recur_end_type,
            $event->recur_count, $event->recur_until, $rawData, $event->rec_id = 0,
            $event->detached_from_rec = 0, $checkOnly = false);
      }

      $status = !empty( $successful);
    }

    // returns boolean status, true if success
    return $status;
  }

  /**
   * Initialize all static elements of a Jcal pro event object,
   * using data from a iCalCreator vevent object
   *
   * @param $event the target Jcal pro event object
   * @param $c the source iCalCreator vevent object
   * @return none
   */
  private function _setEventStaticElements( & $event, $c) {

  // other elements that we won't find in iCal record
  // readjust day, month, year to the UTC values
    $startDate = $this->_getICalProperty( $c, 'dtstart');
    $endDate = $this->_getICalProperty( $c, 'dtend');

    // turn those array into our (mysql) format
    $dates = $this->_iCalDatesToJCalPro( $startDate, $endDate);

    // find about legacy columns
    $startDateTS = jcUTCDateToTs( $dates['start']);
    $day = jcUTCDateToFormatNoOffset( $startDateTS, '%d');
    $month = jcUTCDateToFormatNoOffset( $startDateTS, '%m');
    $year = jcUTCDateToFormatNoOffset( $startDateTS, '%Y');

    // create a $event object, to work with existing update functions
    $event->cal_id = $this->_params->targetCal;
    $event->rec_id = 0;
    $event->detached_from_rec = 0;
    $event->owner_id = $this->_params->owner_id;
    $event->title = $this->_getICalProperty( $c, 'summary');
    $event->description = $this->_getICalProperty( $c, 'description');
    if (is_array( $event->description)) {
      $event->description = $event->description[0];
    }
    $event->contact = $this->_getICalProperty( $c, 'contact');
    $event->url = $this->_getICalProperty( $c, 'url');
    $event->registration_url = '';
    $event->email = $this->_getICalProperty( $c, 'contact', 1); // email address : try using contact #2, if any;
    $event->picture = '';
    $event->cat = $this->_params->targetCat;
    $event->start_date = $dates['start'];
    $event->end_date = $dates['end'];
    $event->day = $day;
    $event->month = $month;
    $event->year = $year;
    $event->approved = 1;
    $event->private = JCL_EVENT_PUBLIC;
    $event->published = 1;
    $event->checked_out = '0';
    $event->checked_out_time = '0000-00-00 00:00:00';
  }

  /**
   * Initialize all elements dealing with recurrency in a Jcal pro event object,
   * using data from a iCalCreator vevent object
   *
   * @param $event the target Jcal pro event object
   * @param $c the source iCalCreator vevent object
   * @return none
   */
  private function _setEventRecurrencyElements( & $event, $c) {

    static $rDaysList = array('SU' => JCL_REC_DAY_TYPE_SUNDAY,  'MO' => JCL_REC_DAY_TYPE_MONDAY,  'TU' => JCL_REC_DAY_TYPE_TUESDAY,  'WE' => JCL_REC_DAY_TYPE_WEDNESDAY,
    'TH' => JCL_REC_DAY_TYPE_THURSDAY,  'FR' => JCL_REC_DAY_TYPE_FRIDAY,  'SA' => JCL_REC_DAY_TYPE_SATURDAY);

    // first set default values for each element,
    // which will possibly overriden when parsing the recurrence rules
    // found in the ical event
    $this->_setDefaultEventRecurrencyElements( $event);


    // find about recurrency information
    $recRules = $this->_getICalProperty( $c, 'rrule');

    // first parse recurrence rules
    if (!empty( $recRules)) {
      $freq = null;
      $rinterval = null;
      $until = null;
      $count = null;
      $rByDay = null;
      $rByMonthDay = null;
      $rByYearDay = null;
      $rByMonth = null;
      $rBySetPos = null;
      foreach( $recRules as $rule) {
      // frequency rule
        if (isset($rule['FREQ'])) {
          $freq = strtolower($rule['FREQ']);
        }
        // number of occurence rule
        if (isset($rule['COUNT'])) {
          $count = intval($rule['COUNT']);
          $event->recur_count = $count;
          $event->recur_end_type = JCL_RECUR_SO_MANY_OCCURENCES;
        }
        // repeat interval rule
        if (isset($rule['RINTERVAL'])) {
          $rinterval = intval($rule['INTERVAL']);
        }
        // repeat until a date rule
        if (isset($rule['UNTIL'])) {
          $until = intval($rule['UNTIL']);
          $event->recur_until = $this->_iCalDateRecordToJCalPro( $until);
        }
        // we don't accept events that recur infinitely
        if (empty($until) && empty( $count) && !empty($freq)) {
        // don't repeat past our maximum date
          $event->recur_until = JCL_DATE_MAX;
          $event->recur_end_type = JCL_RECUR_UNTIL_A_DATE;
        }
        // repeat by day rule
        if (isset($rule['BYDAY'])) {
          if (is_array($rule['BYDAY'])) {
            if (!empty( $rule['BYDAY'])) {
              foreach( $rule['BYDAY'] as $key => $value) {
                if (array_key_exists( $value, $rDaysList)) {
                  $rByDay[] = $rDaysList[$value];
                }
              }
            }
          } else {
            $rByDay = intval($rule['BYDAY']);
          }
        }
        // repeat by year rule
        if (isset($rule['BYYEARDAY'])) {
          if (is_array($rule['BYYEARDAY'])) {
            if (!empty( $rule['BYYEARDAY'])) {
              foreach( $rule['BYYEARDAY'] as $key => $value) {
                if (array_key_exists( $value, $rDaysList)) {
                  $rByYearDay[] = $rDaysList[$value];
                }
              }
            }
          } else {
            $rByYearDay = intval($rule['BYYEARDAY']);
          }
        }
        // repeat by month rule
        if (isset($rule['BYMONTHDAY'])) {
          if (is_array($rule['BYMONTHDAY'])) {
            if (!empty( $rule['BYMONTHDAY'])) {
              foreach( $rule['BYMONTHDAY'] as $key => $value) {
                if (array_key_exists( $value, $rDaysList)) {
                  $rByMonthDay[] = $rDaysList[$value];
                }
              }
            }
          } else {
            $rByMonthDay = intval($rule['BYMONTHDAY']);
          }
        }
        // repeat by month rule
        if (isset($rule['BYMONTH'])) {
          if (is_array($rule['BYMONTH'])) {
            if (!empty( $rule['BYMONTH'])) {
              foreach( $rule['BYMONTH'] as $value) {
                $rByMonth[] = intval($value);
              }
            }
          } else {
            $rByMonth = intval($rule['BYMONTH']);
          }
        }
        // set value for repeat rule
        if (isset($rule['BYSETPOS'])) {
          $rBySetPos = intval(trim($rule['BYSETPOS']));
        }
      }

      // now store the results
      switch ($freq) {
        case 'daily':
          $event->rec_type_select = JCL_REC_TYPE_DAILY;
          $event->rec_daily_period = empty( $rinterval) ? $event->rec_daily_period : $rinterval;
          break;
        case 'weekly':
          $event->rec_type_select = JCL_REC_TYPE_WEEKLY;
          $event->rec_weekly_period = empty( $rinterval) ? $event->rec_weekly_period : $rinterval;
          if (!empty($rByDay)) {
            if (is_array($rByDay)) {
             foreach( $rByDays as $byDay) {
               switch ($byDay) {
                 case 'SU':
                   $event->rec_weekly_on_sunday = 1;
                   break;
                 case 'MO':
                   $event->rec_weekly_on_monday = 1;
                   break;
                 case 'TU':
                   $event->rec_weekly_on_tuesday = 1;
                   break;
                 case 'WE':
                   $event->rec_weekly_on_wednesday = 1;
                   break;
                 case 'TH':
                   $event->rec_weekly_on_thursday = 1;
                   break;
                 case 'FR':
                   $event->rec_weekly_on_friday = 1;
                   break;
                 case 'SA':
                   $event->rec_weekly_on_saturday = 1;
                   break;
               }
             }
            }
          }

          break;

        case 'monthly':
          $event->rec_type_select = JCL_REC_TYPE_MONTHLY;
          $event->rec_monthly_period = empty( $rinterval) ? $event->rec_monthly_period : $rinterval;

          if (!empty($rByDay)) {
            $event->rec_monthly_day_type = is_array($rByDay) ? $rByDay[0] : $rByDay;
          }
          if (!empty($rByMonthDay)) {
            if (is_array($rByMonthDay)) {
              foreach( $rByMonthDay as $monthDay) {
                $event->rec_monthly_day_number = $monthDay;
              }
            } else {
              $event->rec_monthly_day_number = $rByMonthDay;
            }
          }
          if (!empty( $rBySetPos)) {
            if (is_numeric( $rBySetPos)) {
              $event->rec_monthly_type = JCL_REC_ON_SPECIFIC_DAY;
              $event->rec_monthly_day_order = intval( $rBySetPos);
            }
          }

          break;
        case 'yearly':
          $event->rec_type_select = JCL_REC_TYPE_YEARLY;
          $event->rec_yearly_period = empty( $rinterval) ? $event->rec_yearly_period : $rinterval;
          if (!empty($rByDay)) {
            $event->rec_yearly_day_type = is_array($rByDay) ? $rByDay[0] : $rByDay;
          }
          if (!empty($rByYearDay)) {
            $event->rec_yearly_day_number = is_array($rByYearDay) ? $rByYearDay[0] : $rByYearDay;
          }
          if (!empty( $rBySetPos)) {
            if (is_numeric( $rBySetPos)) {
              $event->rec_yearly_type = JCL_REC_ON_SPECIFIC_DAY;
              $event->rec_yearly_day_order = intval( $rBySetPos);
            }
          }
          if (!empty( $rByMonth)) {
            $event->rec_yearly_on_month = is_array($rByMonth) ? $rByMonth[0] : $rByMonth;
          }

          break;
      }

      if (!empty($count)) {
        $event->recur_count = $count;
        $event->recur_end_type = JCL_RECUR_SO_MANY_OCCURENCES;
      }
    }
  }

  /**
   * Initialize all elements dealing with recurrency with default values
   * in a Jcal pro event object
   * Some of them maybe later overriden when injecting
   * data from an iCal object into the jcalpro event object
   *
   * @param $event the target Jcal pro event object
   * @param $c the source iCalCreator vevent object
   * @return none
   */
  private function _setDefaultEventRecurrencyElements( & $event) {

    $event->recur_type = '';
    $event->recur_val = '';
    $event->recur_end_type = JCL_RECUR_SO_MANY_OCCURENCES;
    $event->recur_count = 2;
    $event->recur_until = '';

    // general
    $event->rec_type_select = JCL_REC_TYPE_NONE;

    // daily
    $event->rec_daily_period = 1;

    // weekly
    $event->rec_weekly_period = 1;
    $event->rec_weekly_on_monday = 0;
    $event->rec_weekly_on_tuesday = 0;
    $event->rec_weekly_on_wednesday = 0;
    $event->rec_weekly_on_thursday = 0;
    $event->rec_weekly_on_friday = 0;
    $event->rec_weekly_on_saturday = 0;
    $event->rec_weekly_on_sunday = 0;

    // monthly
    $event->rec_monthly_period = 1;
    $event->rec_monthly_type = JCL_REC_ON_DAY_NUMBER;
    $event->rec_monthly_day_number = 1;
    $event->rec_monthly_day_list = '';
    $event->rec_monthly_day_order = JCL_REC_FIRST;
    $event->rec_monthly_day_type = JCL_REC_DAY_TYPE_DAY;

    // yearly
    $event->rec_yearly_period = 1;
    $event->rec_yearly_on_month = JCL_REC_JANUARY;
    $event->rec_yearly_on_month_list = '';
    $event->rec_yearly_type = JCL_REC_ON_DAY_NUMBER;
    $event->rec_yearly_day_number = 1;
    $event->rec_yearly_day_order = JCL_REC_FIRST;
    $event->rec_yearly_day_type = JCL_REC_DAY_TYPE_DAY;

    // end date
    $event->recur_until = jcServerDateToFormat( extcal_get_local_time(), '%Y-%m-%d %H:%M:%S');

  }

  /**
   * find and returns a given property from an iCal record
   * checking if it exists and handling special cases
   * like decide if array or not array
   *
   * @param $event an iCalCreator.class.php iCal event record
   * @param $property string, the property name
   * @param $index integer, if property may be an array, $index optionnally specify which element to retrieve
   * @return mixed, the property value
   */
  private function _getICalProperty( $event, $property, $index = null) {

    $property = strtolower($property);
    switch (strtoupper($property)) {

      case 'ATTACH':
      case 'ATTENDEE':
      case 'CATEGORIES':
      case 'COMMENT':
      case 'CONTACT':
      case 'DESCRIPTION':
      case 'EXDATE':
      case 'EXRULE':
      case 'RDATE':
      case 'RELATED-TO':
      case 'REQUEST-STATUS':
      case 'RESOURCES':
      case 'RRULE':
        $tmp = isset($event->$property) ? $event->$property : null;
        // if user has requested one specific item in the property array
        if (!is_null($index)) {
          $tmp = isset($tmp[$index]) ? $tmp[$index] : $tmp;
        }
        // there is actually an array of property,
        // we must iterate the array to only return an
        // array of values, excluding the PARAMS
        $value = array();
        if (!empty($tmp)) {
          foreach( $tmp as $item) {
            $value[] = $item['value'];
          }
        }
        break;
      // for dtstart and dtend, we need the params field as well,
      // as iCalCreator sotres TZID in there
      case 'DTSTART':
      case 'DTEND':
        $value = isset($event->$property) ? $event->$property : null;
        break;
      default:
        $tmp = isset($event->$property) ? $event->$property : null;
        // there is only one instance of the property,
        // we can directly return its 'value' element
        $value = !empty( $tmp) ? $tmp['value'] : null;
        break;
    }

    return $value;
  }

  /**
   * Turns start and end dates as per iCal property format
   * into UTC datetime string, needed for DB storage
   * Handles detecting all-day events
   *
   * @param $dateStart
   * @param $dateEnd
   * @return array start and end date as strings
   */
  private function _iCalDatesToJCalPro( $startDate, $endDate) {

    $jCalDates = array( 'start' => null, 'end' => null);

    // work on start date first, to identify all-day event
    if (!isset( $startDate['value']['hour']) && !isset( $startDate['value']['min']) && !isset( $startDate['value']['sec'])) {
    // this is an all-day event, mark it as such
      $jCalDates['end'] = JCL_ALL_DAY_EVENT_END_DATE;
      $startDate['value']['hour'] = 0;
      $startDate['value']['min'] = 0;
      $startDate['value']['sec'] = 0;
    }

    // now create start date
    $jCalDates['start'] = $this->_iCalDateRecordToJCalPro( $startDate);

    // if not already done, create end date
    if (is_null($jCalDates['end'])) {
      $jCalDates['end'] = $this->_iCalDateRecordToJCalPro( $endDate);
    }
    return $jCalDates;
  }

  /**
   * Turns an iCalCreator date record into its
   * jCal equivalent, allowing database storage
   *
   * @param $date array with date details
   * @return string a datetime string
   */
  private function _iCalDateRecordToJCalPro( $date) {

    global $mainframe;

    // quick check
    if (empty( $date)) {
      return null;
    }

    // check timezone
    $iCalOffset = $this->_calculateTZOffset( $date);

    // create time, assuming everything is UTC
    $dateTime = jcTimeToUTCNoDst( $date['value']['hour'], $date['value']['min'], $date['value']['sec'], $date['value']['month'], $date['value']['day'], $date['value']['year']);

    // make it a timestamp
    $ts = jcUTCDateToTs( $dateTime);

    // if the iCal time has an offset in it, compensate for it so we do have an UTC time
    // suitable for storage in database
    $ts = $ts - $iCalOffset;
    
    // turn it back to date time
    return jcUTCDateToFormatNoOffset( $ts, '%Y-%m-%d %H:%M:%S');
  }

  /**
   * Calculates the timezone offset based on the date
   * May require using a vtimezone, if any, found in
   * in the ics file itself (Outlook files for instance)
   *
   * @param $date a date record, array indexed on year, month, day, etc
   * @return string
   */
  private function _calculateTZOffset( $date) {

    static $foundZones = null;

    $offset = null;
    if (!empty( $date['value']['tz'])) {

    // UTC time
      if (strtolower($date['value']['tz']) == 'z') {
        $offset = 0;
      }
      // offset is directly listed : ex +0100
      $matches = null;
      if (preg_match( '/[+-]?[0-9]{4}/', JString::trim($date['value']['tz']), $matches)) {
        $offset = $matches[0];
        $sign = $offset < 0 ? -1 : 1;
        $offset = abs( $offset);
        $offsetHours = floor( $offset / 100);
        $offsetMinutes = $offset - 100 * $offsetHours;

        $offset = $sign * 3600 * $offsetHours + 60 * $offsetMinutes;
      }
    }

    // something else, in the TZID parameter
    if (is_null( $offset) && !empty($date['params']['TZID'])) {
    // if we have not found an offset yet, search for the timezone name
    // in php list of timezones
      if (is_null( $offset) && !empty( $this->_timezones)) {
      // found the timezone name amongst the timezones definition embedded in the ical file
      // @TODO now we must decode it ! this requires implementing a full RRULE parser
      // to be able to identify dst transition dates as used in Outlook ics files
      // workaround : we search for cities, in PHP timezone list
        $found = false;
        // first check if we have not already recognized this timezone
        if (!empty( $foundZones) && array_key_exists( $date['params']['TZID'], $foundZones)) {
          $offset = $foundZones[$tzid];
          $found = true;
        }
        // iterate through all timezones records found in ics files
        if (!$found) {
          $phpZones = jclGetPhpTimezonesList();
          foreach( $this->_timezones as $tzid => $tz) {
          // explode cities, and search for them
            $cities = explode( ',', $tzid);
            if (!empty( $cities)) {
              foreach( $cities as $city) {
                $city = JString::trim(JString::strtolower( $city));
                foreach( $phpZones as $phpZoneId => $phpZoneDetails) {
                  if (JString::strpos( JString::strtolower( $phpZoneId), $city) !== false) {
                  // found a city name in our list, just use it and break
                    $offset = $phpZoneDetails['offset'];
                    $foundZones[$tzid] = $offset;
                    $found = true;
                    break(3);
                  }
                }
              }
            }
          }
        }
      }
    }

    // return calculated offset value
    return intval( $offset);

  }

  /**
   * Clean an incoming uid from an ical event
   * as they may have spaces, cr, lf, etc
   *
   * @param $UID string
   * @return string
   */
  private function _cleanUID( $UID) {

  // the uid is cut off at 240 caracters to make room for the trailing sequence number added for children, in case
  // of repeating events
    return Jfilteroutput::stringURLSafe( JString::substr($UID, 0, 240));
  }
}
